﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;

namespace NConfigGenerator
{
    [DebuggerDisplay("{RootNode.Name}")]
    public sealed class ConfigTree
    {
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly ConfigNode _rootNode;

        private ConfigTree(ConfigNode rootNode)
        {
            _rootNode = rootNode;
        }

        public ConfigNode RootNode
        {
            [DebuggerStepThrough]
            get { return _rootNode; }
        }

        public static ConfigTree Create(string rootName)
        {
            return new ConfigTree(new ElementConfigNode(null, rootName));
        }

        #region Inference code
        public static ConfigTree InferFromXml(string xml)
        {
            if (xml == null)
                throw new ArgumentNullException("xml");

            var xdoc = new XmlDocument();
            xdoc.LoadXml(xml);

            XmlElement rootElement = xdoc.DocumentElement;
            if (rootElement == null)
                throw new XmlException("Root element not found");

            ConfigNode rootNode = CreateNode(rootElement, null);
            InferRecursively(rootElement, rootNode);

            var configTree = new ConfigTree(rootNode);
            return configTree;
        }

        private static void InferRecursively(XmlElement element, ConfigNode configNode)
        {
            if (configNode == null)
                throw new Exception("Cannot infer type of config to generate for the " + element.Name + " element");

            ElementType elementType = configNode is ElementConfigNode ? ElementType.Element : ElementType.Collection;

            foreach (XmlAttribute attribute in element.Attributes)
                configNode.Attributes.Add(CreateNodeAttribute(attribute));

            XmlNodeList childElements = element.ChildNodes;

            if (elementType == ElementType.Element)
            {
                var elementNode = (ElementConfigNode)configNode;
                foreach (XmlElement childElement in childElements)
                {
                    ConfigNode childConfigNode = CreateNode(childElement, configNode);
                    InferRecursively(childElement, childConfigNode);
                    elementNode.Children.Add(childConfigNode);
                }
            } else if (elementType == ElementType.Collection)
            {
                var collectionNode = (CollectionConfigNode)configNode;
                var firstChildElement = (XmlElement)childElements[0];
                ConfigNode childConfigNode = CreateNode(firstChildElement, configNode);
                InferRecursively(firstChildElement, childConfigNode);
                collectionNode.Child = childConfigNode;
            }
        }

        private static ConfigNode CreateNode(XmlElement element, ConfigNode parentNode)
        {
            ElementType elementType = InferElementType(element);
            if (elementType == ElementType.Element)
                return new ElementConfigNode(parentNode, element.Name);
            if (elementType == ElementType.Collection)
                return new CollectionConfigNode(parentNode, element.Name);
            return null;
        }

        private static ConfigNodeAttribute CreateNodeAttribute(XmlAttribute xmlAttribute)
        {
            Match match = _optionalAttributePattern.Match(xmlAttribute.Value);
            string attributeValue = match.Success ? match.Groups[1].Value : xmlAttribute.Value;
            var nodeAttribute = new ConfigNodeAttribute(xmlAttribute.Name, InferAttributeType(attributeValue)) {
                Optional = match.Success
            };
            return nodeAttribute;
        }

        private static readonly Regex _optionalAttributePattern = new Regex(@"^\[(.+)\]$");

        private static AttributeType InferAttributeType(string value)
        {
            if (string.IsNullOrEmpty(value))
                return AttributeType.String;

            long integerValue;
            if (long.TryParse(value, out integerValue))
                return AttributeType.Integer;

            double doubleValue;
            if (double.TryParse(value, out doubleValue))
                return AttributeType.Floating;

            bool boolValue;
            if (bool.TryParse(value, out boolValue))
                return AttributeType.Boolean;

            string upperValue = value.ToUpper();
            if (upperValue == "Y" || upperValue == "N")
                return AttributeType.Boolean;

            DateTime dateTimeValue;
            if (DateTime.TryParse(value, out dateTimeValue))
                return AttributeType.DateTime;

            if (_typeNamePattern.IsMatch(value))
                return AttributeType.Type;

            return AttributeType.String;
        }

        private static readonly Regex _typeNamePattern = new Regex(@"^\s*[\w._]+\s*,\s*[\w._]+");

        private static ElementType InferElementType(XmlElement element)
        {
            XmlNodeList childElements = element.ChildNodes;

            //If the element only has a single child element, then its just an element type
            if (childElements.Count <= 1)
                return ElementType.Element;

            //If all the child element names are the same, then its a collection
            //If all the child element names are different, then its an element
            //If there are both repeating child element names and different child element names, then
            //its an invalid element and an InferenceException should be thrown

            //Keep track of unique element names, to check for repetitions
            var uniqueElementNames = new List<string>(childElements.Count);
            bool hasRepetitions = false;
            bool isCollection = true;

            string elementName = childElements[0].Name;
            uniqueElementNames.Add(elementName);
            for (int i = 1; i < childElements.Count; i++)
            {
                string childElementName = childElements[i].Name;
                if (uniqueElementNames.Contains(childElementName))
                    hasRepetitions = true;
                else
                    uniqueElementNames.Add(childElementName);

                if (!elementName.Equals(childElementName, StringComparison.InvariantCulture))
                    isCollection = false;
            }

            if (isCollection)
                return ElementType.Collection;
            if (!hasRepetitions)
                return ElementType.Element;

            string errorMessage = string.Format("Element '{0}' is invalid. It has both repeating items as well as different items",
                element.Name);
            throw new InferenceException(errorMessage);
        }

        private enum ElementType
        {
            Element,
            Collection,
        }
        #endregion
    }
}